/*
 * Fire (Flexible Interface Rendering Engine) is a set of graphics widgets for creating GUIs for j2me applications. 
 * Copyright (C) 2006-2008 Bluevibe (www.bluevibe.net)
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 */

/**
 * 
 */
package gr.fire.browser;

import gr.fire.browser.util.HttpClient;
import gr.fire.browser.util.Page;
import gr.fire.browser.util.PageListener;
import gr.fire.browser.util.Request;
import gr.fire.core.CommandListener;
import gr.fire.core.Component;
import gr.fire.core.Container;
import gr.fire.core.FireScreen;
import gr.fire.core.Panel;
import gr.fire.ui.Alert;
import gr.fire.ui.ProgressbarAnimation;
import gr.fire.ui.TransitionAnimation;
import gr.fire.util.FireConnector;
import gr.fire.util.Lang;
import gr.fire.util.Log;
import gr.fire.util.StringUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.Vector;

import javax.microedition.io.HttpConnection;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Font;

import org.kxml2.io.KXmlParser;

/**
 * The Browser parses XHTML from a given stream and renders it.
 * It has some basic rendering rules that are choosen to improve the 
 * readability and usability of a page when rendered for a small screen. <br/>
 * 
 * * The default width of the browser is the width of the screen.<br/>
 *    
 * * TextPrimitive width is set as the width of the Browser unless noted otherwise 
 *   by the style of the tag.<br/>
 *   
 * * TextPrimitive height is the height of the text calculated using the width 
 *   (according to the rules above) and the font of the text.  <br/>
 *   
 * * ImagePrimitive width is the width of the image unless noted otherwise by the style of the tag. <br/>
 * 
 * @author padeler
 */
public class Browser implements CommandListener, PageListener
{
	
	/**
	 * Flag for the imageLoadingPolicy field. 
	 * No images are loaded.
	 */
	public static final byte NO_IMAGES=0x00; 
	/**
	 * Flag for the imageLoadingPolicy field. 
	 * Load images imediatelly
	 */
	public static final byte LOAD_IMAGES=0x01; 
	
	/**
	 * Flag for the imageLoadingPolicy field. This is the default browser behaivior.
	 * Load images in a seperate thread after the full parsing of the page. 
	 * The browser will return the Page object and start a thread to load the rest of the images.
	 * This will apply only for images that the width and height attributes are set.
	 * Images that do not have their width and height attributes set, will be loaded imediatelly.
	 */
	public static final byte LOAD_IMAGES_ASYNC=0x02;

	/**
	 * The listener that will receive the events generated by the page rendered by this browser (i.e. link clicks etc) 
	 * The listener will also receive the submit events generated by forms in the rendered pages. 
	 * If null, then the form handles the submit, with the default way (calls browser.displayPage()) 
	 */
	CommandListener listener = this; 
	PageListener pageListener = this;
	
	/**
	 * If this field is set to false, then the browser will not request the images inside each page
	 */
	byte imageLoadingPolicy = LOAD_IMAGES_ASYNC;  
	
	/**
	 * The HttpClient used to request resources.
	 */
	HttpClient httpClient;
	
	private Hashtable knownTags = new Hashtable();
	
	private int viewportWidth=0; 
	
	
	/* ****** HTML Parsing support variables ******* */  
	private Vector tagStack = new Vector();
	private ProgressbarAnimation gauge=null;
	
	/**
	 * Constructor of a Browser instance with the default HttpClient.
	 * The CommandListener and PageListener are set to this new Browser instance.
	 * @see HttpClient
	 * @see PageListener
	 * @see CommandListener
	 */
	public Browser()
	{
		this(new HttpClient(new FireConnector()));
	}
	
	/**
	 * Constructor of a Browser instance with the supplied httpclient. All other parameters are set to the default
	 * @see #Browser()
	 * @param httpClient
	 */
	public Browser(HttpClient httpClient)
	{
		this.httpClient = httpClient; 
		
		{
			Class ie = new InlineTag().getClass();
			
			registerTag(InlineTag.TAG_A,ie);
			registerTag(InlineTag.TAG_B,ie);
			registerTag(InlineTag.TAG_BR,ie);
			registerTag(InlineTag.TAG_EM,ie);
			registerTag(InlineTag.TAG_I,ie);
			registerTag(InlineTag.TAG_IMG,ie);
			registerTag(InlineTag.TAG_SPAN,ie);
			registerTag(InlineTag.TAG_STRONG,ie);
			registerTag(InlineTag.TAG_BIG,ie);
			registerTag(InlineTag.TAG_SMALL,ie);
			registerTag(InlineTag.TAG_TT,ie);
			registerTag(InlineTag.TAG_U,ie);
			registerTag(InlineTag.TAG_TD,ie);
			registerTag(InlineTag.TAG_INPUT,ie);
			registerTag(InlineTag.TAG_BUTTON,ie);
			registerTag(InlineTag.TAG_TEXTAREA,ie);
			registerTag(InlineTag.TAG_CENTER,ie);
			registerTag(InlineTag.TAG_LABEL,ie);
			registerTag(InlineTag.TAG_OPTION,ie);
			registerTag(InlineTag.TAG_SELECT,ie);
		}		
		
		{
			Class be = new BlockTag().getClass();
		
			registerTag(BlockTag.TAG_P,be);		
			registerTag(BlockTag.TAG_BODY,be);		
			registerTag(BlockTag.TAG_TABLE,be);	
			registerTag(BlockTag.TAG_TR,be);
			registerTag(BlockTag.TAG_DIV,be);		
			registerTag(BlockTag.TAG_TITLE,be);		
			registerTag(BlockTag.TAG_META,be);		
			registerTag(BlockTag.TAG_STYLE,be);
			registerTag(BlockTag.TAG_SCRIPT,be);
			registerTag(BlockTag.TAG_H1,be);		
			registerTag(BlockTag.TAG_H2,be);		
			registerTag(BlockTag.TAG_H3,be);		
			registerTag(BlockTag.TAG_H4,be);		
			registerTag(BlockTag.TAG_H5,be);		
			registerTag(BlockTag.TAG_H6,be);
			registerTag(BlockTag.TAG_HR,be);
			registerTag(BlockTag.TAG_FORM,be);
		}		
		
		{
			Class le = new ListBlockTag().getClass();
			registerTag(ListBlockTag.TAG_UL,le);		
			registerTag(ListBlockTag.TAG_LI,le);		
			registerTag(ListBlockTag.TAG_OL,le);		
			registerTag(ListBlockTag.TAG_DL,le);		
			registerTag(ListBlockTag.TAG_DT,le);		
			registerTag(ListBlockTag.TAG_DD,le);		
		}
	}
	
	/**
	 * Registers an XML tag to be handled by an instance of the given class. The class MUST be a subclass of Tag
	 * @see HtmlUtil
	 * @param name name of the tag
	 * @param cl the class that will handle the action
	 */
	public void registerTag(String name,Class cl) 
	{
		if(name!=null && cl!=null)
		{
			try
			{
				Tag t = (Tag)cl.newInstance();
			} catch (Exception e)
			{
				Log.logError("Failed to register class for tag "+name+". ",e);
				throw new IllegalArgumentException("Class must be an instantiable subclass of Tag. "+e.getMessage());
			}
			knownTags.put(name,cl);
		}
		else throw new NullPointerException("Tag name and class cannot be null");
	}
	
	private void showGauge(String message)
	{
		if(gauge!=null) hideGauge();
		
		gauge = new ProgressbarAnimation(message);
		
		Font font = FireScreen.getTheme().getFontProperty("titlebar.font");

		FireScreen screen = FireScreen.getScreen();
		int sw = screen.getWidth();
		int mw = font.stringWidth(message);
		gauge.setWidth(sw);
		gauge.setHeight(font.getHeight());
		screen.addComponent(gauge,6);
	}
	
	private void hideGauge()
	{
		if(gauge!=null){
			FireScreen.getScreen().removeComponent(gauge);
			gauge=null;
		}
	}

	/**
	 * Loads the page from the given URL using the supplied method and reqest parameters and data. 
	 * This method will use the supplied HttpClient {@link #httpClient} to make the request and then render the page.<br/> 
	 * 
	 * The resulting will be added to added to a Page instance and returned to the caller.
	 * It will not handle "meta" tag information, but will return them inside the Page instance. <br/>
	 * 
	 * This method is <b>synchronized</b> on the browser instance.
	 * 
	 * @param url 
	 * @param method The method can be HttpConnection.GET or HttpConnection.POST
	 * @param requestParameters params for the http request header 
	 * @param data if the method is HttpConnection.POST the post data if any must be in this byte array 
	 * @return
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 * @throws Exception
	 */
	public synchronized Page loadPage(String url,String method,Hashtable requestParameters,byte []data) throws UnsupportedEncodingException,IOException,Exception
	{
		if(url.startsWith("ttp://") || url.startsWith("ttps://"))
		{
			url = "h"+url;
			Log.logWarn("Malformed url resolved to: "+url);
		}
		
		Page page = new Page(url);

		/* ********** Display a gauge *************** */
		showGauge(StringUtil.proccessUrl(url,true));
		
		/* ************************** Request the resource **************************** */
		Request currentRequest = null;
		try{
			currentRequest = httpClient.requestResource(url,method,requestParameters,data,true);
			if(currentRequest==null) {
				Log.logWarn("Failed to get resource from "+url);
				return null;
			}
			InputStream in = currentRequest.getInputStream();
			if (in == null)
			{
				Log.logWarn("Failed to read data from "+url);
				return null;
			}
			
			Log.logDebug("Base URL is: "+currentRequest.getBaseURL());
			String encoding= currentRequest.getEncoding();
	
			return loadPageFromStream(page,in,encoding);
		}catch(Exception e){
			Log.logError("Failed to request page "+url+".",e);
			throw e;
		}finally{
			if(currentRequest!=null){
				try{
					currentRequest.close();
				}catch(IOException ex){
					Log.logWarn("Connection not closed!", ex);
				}
				currentRequest=null;
			}
			
			hideGauge();
		}
	}
	
	/**
	 * Loads a page from the given InputStream using the given encoding.
	 * 
	 * This method is <b>synchronized</b> on the browser instance.
	 * 
	 * @param in
	 * @param encoding
	 * @return A Page instance containing the result of the request
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 * @throws Exception
	 */
	public synchronized Page loadPage(InputStream in,String encoding) throws UnsupportedEncodingException,IOException,Exception
	{
		Page page = new Page();
		/* ********** Display a gauge *************** */
		showGauge("Loading...");
		try{
			return loadPageFromStream(page,in,encoding);
		}catch(Exception e){
			Log.logError("Failed to request page from stream.",e);
			throw e;
		}finally{
			hideGauge();
		}
	}
	
	/*
	 * The main loop of the Browser module. This uses the XmlPullParser to parse the xml from the inputstream and 
	 * then iterates through the tags of the document. Each known tag is handled by the class that is registered to handle it
	 * using the registerTag method.
	 */
	private Page loadPageFromStream(Page page,InputStream in,String encoding) throws UnsupportedEncodingException,IOException,Exception
	{
		/* ******************************** clean old page stuff here **************************** */
		tagStack.removeAllElements();
		if(viewportWidth<=0)
		{
			viewportWidth = FireScreen.getScreen().getWidth();
		}
		gauge.progress();

		InputStreamReader reader=null;
		try{
			Log.logDebug("Using Encoding: "+encoding);
			reader = new InputStreamReader(in, encoding);
			
			KXmlParser parser = new KXmlParser();
			parser.setInput(reader);
			parser.setFeature(org.xmlpull.v1.XmlPullParser.FEATURE_RELAXED,true);

			int type=-1,oldType=-1;
		
			/* ********** Main XML parsing loop **************** */ 
			while (true)
			{
				if(type==oldType) type= parser.next(); // only progress parser if a tag didnt already call parser.next()
				oldType = type; // some tags call parser.next(), old type helps keep track if parser.next() was called.
				
				if(type==KXmlParser.START_TAG) /* **** Handle Opening TAGs ***** */
				{
					String name = parser.getName().toLowerCase();;
					Class tagClass = (Class)knownTags.get(name);
					if(tagClass!=null)
					{
						try
						{
							Tag t = (Tag)tagClass.newInstance();
							t.handleTagStart(this,page,parser);
							pushTag(t);
						}catch(InstantiationException e)
						{
							Log.logError("Failed to instantiate a Tag class for tag name "+name+".",e);
						} catch (Exception e)
						{
							Log.logError("Exception while handling tag start "+name,e);
						}
					}
					else Log.logWarn("Unknown Opening TAG "+name);
				}
				else if(type==KXmlParser.END_TAG) /* **** Handle Closing TAGs ***** */
				{
					String name = parser.getName().toLowerCase();;
					Tag t = (Tag)topTag();
					if(t!=null && name.equals(t.getName()))
					{
						t.handleTagEnd(this,page,parser);
						popTag();
					}
					else Log.logWarn("Unknown Closing TAG "+name+" expected "+(t==null?"none":t.getName()));
				}
				else if(type==KXmlParser.TEXT) /* **** Handle Text inside a TAG ***** */
				{
					Tag top = (Tag)topTag();
					
					String txt = parser.getText();
					if(top!=null && txt.length()>0)
					{
						top.handleText(top,txt);
					}
				}
				else if(type==KXmlParser.END_DOCUMENT)
				{
					break; // parsing completed.
				}
				else /* **** Default action, just log the unknown type and continue **** */
				{ 
					Log.logWarn("Unknown tag "+parser.getName() +" type " + type);
				}
				
				type  = parser.getEventType(); // get type again since some tags call parser.next() 
			}
			
			gauge.progress();
			
			Log.logDebug("=>End Of Document<=");
			
		}finally{
			try{
				if(reader!=null) reader.close();
			}catch(Throwable e) {}	
		}
		
		page.setAbsolutUrl(httpClient.getCurrentURL());
		
		if(imageLoadingPolicy==LOAD_IMAGES_ASYNC)
			page.startAsyncImageLoad(httpClient);

		
		return page;
	}
	
	/**
	 * When a Tag is pushed it is considered to be inside the last one pushed. 
	 * The Tag implementation is responsible for doing so.
	 *  
	 * @param node
	 */
	private void pushTag(Tag node)
	{
		tagStack.addElement(node);
	}
	
	public Tag topTag()
	{
		if(tagStack.size()>0)
			return (Tag)tagStack.lastElement();
		// else
		return null;
	}

	
	private Tag popTag()
	{
		int size = tagStack.size();
		if(size>0)
		{
			Tag tc = (Tag)tagStack.lastElement();
			tagStack.removeElementAt(size-1);
			
			if(size<=3) gauge.progress(); // easy (but not so aquarate) method to show progress relative the parsing of the page... 
			
			return tc;
		}
		return null;
	}
		
	/**
	 * The Browser instance is the default CommandListener for any rendered page. Use setListener method to use your
	 * own custom listener.
	 * @see #setListener(CommandListener)
	 * @see gr.fire.core.CommandListener#commandAction(javax.microedition.lcdui.Command, gr.fire.core.Component)
	 */
	public void commandAction(Command command, Component c)
	{
		if(command instanceof gr.fire.browser.util.Command)
		{ // only handle known command types
			gr.fire.browser.util.Command cmd = (gr.fire.browser.util.Command)command;
			String url = cmd.getUrl();
			loadPageAsync(url,HttpConnection.GET,null,null);
		}
	}
	/**
	 * The Browser instance if the default PageListener for loadPageAsync requests. Use the setPageListener method to
	 * use your own custom pageListener
	 * @see PageListener
	 * @see #setPageListener(PageListener)
	 */
	public void pageLoadCompleted(String url,String method,Hashtable requestParams, Page page)
	{		
		if(page!=null)
		{
			Container cnt = page.getPageContainer();
			String title= page.getPageTitle();
			Log.logInfo("Loaded Page ["+url+"]["+title+"]");

			if(cnt!=null)
			{
				Panel panel = new Panel(cnt,Panel.VERTICAL_SCROLLBAR|Panel.HORIZONTAL_SCROLLBAR,true);
				panel.setLabel(title);
				
				Component current = FireScreen.getScreen().getCurrent();
				Command left=null,right=null;
				if(current!=null)
				{
					left = current.getLeftSoftKeyCommand();
					right = current.getRightSoftKeyCommand();
					
				}
				
				panel.setLeftSoftKeyCommand(left);
				panel.setRightSoftKeyCommand(right);
				FireScreen screen = FireScreen.getScreen();
				Component last = screen.getCurrent();
				
				if(last!=null) // show a transition animation
				{
					panel.setAnimation(new TransitionAnimation(last,panel,TransitionAnimation.TRANSITION_SCROLL|TransitionAnimation.TRANSITION_RIGHT));
				}
				
				FireScreen.getScreen().setCurrent(panel);
				panel.setCommandListener(listener);
				panel.setDragScroll(true);
				return;
			}
		}
		// Error case. Alert user
		String t=url;
		if(url.length()>15) t = url.substring(0,15)+"...";  
		FireScreen.getScreen().showAlert(Lang.get("Failed to load page")+": "+t,Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);
	}
	
	/**
	 * This is the asynchronous version of the loadPage{@link #loadPage(String, String, Hashtable, byte[])} method.
	 * It will start a new thread to handle the request and it will send the result Page to the registered PageListener 
	 * instead of returning it to the caller.
	 * 
	 * @see #loadPage(InputStream, String)
	 * 
	 * @param url
	 * @param method
	 * @param requestParameters
	 * @param data
	 */
	public void loadPageAsync(final String url,final String method,final Hashtable requestParameters,final byte []data)
	{
		Thread th = new Thread()
		{
			public void run()
			{
				try
				{
					Page pageMeta  =loadPage(url,method,requestParameters,data);
					if(pageListener!=null) pageListener.pageLoadCompleted(url,method,requestParameters,pageMeta);
					else pageLoadCompleted(url,method,requestParameters,pageMeta); // defauly handler
					
					if(pageMeta!=null)
					{
						if(pageMeta.getRefresh()!=null)
						{
							int seconds = pageMeta.getRefreshSeconds();
							
							if(seconds>0) try{Thread.sleep(seconds*1000);}catch(InterruptedException e){}
							Component current = FireScreen.getScreen().getCurrent();
							if((current instanceof Panel && ((Panel)current).getComponent(0)==pageMeta.getPageContainer()) || current==pageMeta.getPageContainer()) // only execute refresh, if the user is still on the same page
							{
								loadPage(pageMeta.getRefresh(),HttpConnection.GET,null,null);
							}
							else 
							{
								Log.logWarn("Ignoring refresh to "+pageMeta.getRefresh() );
							}
						}
					}
				} catch (Throwable er)
				{
					if(pageListener!=null) pageListener.pageLoadFailed(url,method,requestParameters,er);
					else pageLoadFailed(url,method,requestParameters,er); // defauly handler
				}
			}
		};
		th.start();
	}

	
	public void commandAction(Command cmd, Displayable d)
	{
	}

	/**
	 * The Browser renders each page based on a set width and unbounded height. The viewportWidth is 
	 * the width of a rendered page. If the page contains elements that do not fit in the viewpoerWidth
	 * It will increase the width of the resulting page ignoring the viewportWidth.
	 *  
	 * @return
	 */
	public int getViewportWidth()
	{
		return viewportWidth;
	}
	

	/**
	 * Sets the width of the screen that the browser will use to render properly each Page.
	 * @param viewportWidth
	 */
	public void setViewportWidth(int viewportWidth)
	{
		this.viewportWidth = viewportWidth;
	}

	/**
	 * Returns the CommandListener that is set to handle the Browser requests
	 * @return
	 */
	public CommandListener getListener()
	{
		return listener;
	}

	/**
	 * Overides the default listener for link and form events for all rendered pages of this Browser intance.
	 * The default listener is the Browser instance itself.
	 * 
	 * @param listener The CommandListener for link and form events. If null then the Browser instance if used (default)
	 */
	public void setListener(CommandListener listener)
	{
		if(listener==null) listener=this;
		this.listener = listener;
	}

	/**
	 * Returns the HttpClient instance that this Browser instance will use to make Http Requests to http servers. 
	 * @return
	 */
	public HttpClient getHttpClient()
	{
		return httpClient;
	}

	/**
	 * Sets the HttpClient of this Browser instance.
	 * @see #getHttpClient()
	 * @param httpClient
	 */
	public void setHttpClient(HttpClient httpClient)
	{
		this.httpClient = httpClient;
	}

	/**
	 * There are different policies on loading the images of a Page.<br/> 
	 * - Browser.NO_IMAGES <br/>
	 * - Browser.LOAD_IMAGES<br/>
	 * - Browser.LOAD_IMAGES_ASYNC  (default) <br/>
	 * 
	 * The load LOAD_IMAGES_ASYNC will skip images with preset width and height (as img tag properties) and will
	 * try to load them after the Page is done loading. This greatly speeds up Page loading. 
	 * 
	 * @return
	 */
	public byte getImageLoadingPolicy()
	{
		return imageLoadingPolicy;
	}

	/**
	 * @see #getImageLoadingPolicy()
	 * @param imageLoadingPolicy
	 */
	public void setImageLoadingPolicy(byte imageLoadingPolicy)
	{
		this.imageLoadingPolicy = imageLoadingPolicy;
	}

	/**
	 * Returns the PageListener for loadPageAsync requests. The default pagelistener is the Browser.
	 * @see  #pageLoadCompleted(String, String, Hashtable, Page)
	 * @see #pageLoadFailed(String, String, Hashtable, Throwable)
	 * @return
	 */
	public PageListener getPageListener()
	{
		return pageListener;
	}

	/**
	 * @see #getPageListener()
	 * @param pageListener
	 */
	public void setPageListener(PageListener pageListener)
	{
		if(pageListener==null) pageListener=this;
		this.pageListener = pageListener;
	}

	public void pageLoadFailed(String url, String method, Hashtable requestParams, Throwable error)
	{
		if(error instanceof OutOfMemoryError)
		{
			System.gc();
			try
			{
				Log.logError("Out of Memory! Request to URL ["+url+"] failed",error);
				FireScreen.getScreen().showAlert(Lang.get("Could not load page. Out of memory!"),Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);
			}catch (OutOfMemoryError e)
			{
				System.gc();
			}			
		}
		else 
		{
			Log.logError("Request to URL ["+url+"] failed",error);
			FireScreen.getScreen().showAlert(Lang.get("Error loading page. ")+" "+error.getMessage(),Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);			
		}
	}
}